package steveshrader.budget.client;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import steveshrader.budget.client.dto.Expense;
import steveshrader.budget.client.dto.Vendor;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.DisclosurePanel;
import com.google.gwt.user.client.ui.DockPanel;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Hyperlink;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.MultiWordSuggestOracle;
import com.google.gwt.user.client.ui.PushButton;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.SuggestOracle;
import com.google.gwt.user.client.ui.TextArea;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.ToggleButton;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
import com.google.gwt.user.datepicker.client.CalendarUtil;
import com.google.gwt.user.datepicker.client.DateBox;


/**
 * Entry point classes define <code>onModuleLoad()</code>.
 */
public class BudgetAppEntry implements EntryPoint {
        private final BudgetServiceAsync budgetService = GWT.create(BudgetService.class);       //Create a remote service proxy to talk to the server-side Budgeting service.
       
        private VerticalPanel statusLayout = new VerticalPanel();                                       //display overall application messages such as server connect errors
        private ScrollPanel statusScroller = new ScrollPanel(statusLayout);                     //limit the size of the statusPanel in case there are lots of errors
        private VerticalPanel addExpenseLayout = new VerticalPanel();                           //hold components needed to add an expense
        private TextArea bulkExpenseTextArea = new TextArea();                                          //multiple expenses can be entered in the specified format
        private PushButton addBulkExpensesButton = new PushButton(
                        new Image("addIconUp.png"), new Image("addIconDown.png"));                      //button to submit bulk expenses for processing
        private PushButton addIndividualExpenseButton = new PushButton(
                        new Image("addIconUp.png"), new Image("addIconDown.png"));                      //button to submit individual expense for processing
        private MultiWordSuggestOracle vendorOracle = new MultiWordSuggestOracle();     //list Vendor names for selection
        private DateBox newExpenseDate = new DateBox();                                                         //enter info for new Expense
        private TextBox newExpenseAmount = new TextBox();                                                       //enter info for new Expense
        private SuggestBox newExpenseVendor = new SuggestBox(vendorOracle);                     //enter info for new Expense
        private ListBox newExpenseExpenseType = new ListBox(false);                                     //enter info for new Expense
        private ListBox newExpensePaymentType = new ListBox(false);                                     //enter info for new Expense
        private DateBox expenseListStartDate = new DateBox();                                           //start date range of displayed expenses
        private DateBox expenseListEndDate = new DateBox();                                                     //end date range of displayed expenses
        private SuggestBox expenseListVendorFilter = new SuggestBox(vendorOracle);      //filter the vendors displayed in the expense list
        private FlexTable expenseListTable = new FlexTable();                                           //display all Expenses over a given time period
        private FlexTable expenseSummaryTable = new FlexTable();                                        //display Expense Summary information
        private DockPanel displayExpenseLayout = new DockPanel();                                       //everything needed to display and filter expense information will go in here
        private final ToggleButton toggleDeleteExpenseButton = new ToggleButton(
                        new Image("deleteIconUp.png"), new Image("deleteIconDown.png"));        //enable or disable delete expense ability
       
        DateTimeFormat dateFormat = DateTimeFormat.getFormat("MM/dd/yyyy");                                             //format all Dates display and entry
        private DateBox.DefaultFormat dateBoxFormat = new DateBox.DefaultFormat(dateFormat);    //apply default format to DateBox

        private List<PushButton> deleteExpenseButtons;  //holds all delete PushButtons so the can all be toggled visible/not visible at the same time
        private Map<String, Vendor> vendors = new HashMap<String, Vendor>();    //all Vendors in the DB to select from...mapped as vendorName->Vendor
       
        private String[] expenseTypes = {"Charity", "Clothes", "Doctor", "Education", "Entertainment", "Gas", "Grocery", "Home", "Insurance", "Other", "Restaurant", "Utility", "Vehicle"};
        private String[] paymentTypes = {"Cash", "Check", "Discover", "MasterCard", "Visa"};

        private enum StatusType {
                STATUS ("Application-statusText"),
                ERROR  ("Application-errorText");
               
                private final String styleName;
               
                StatusType(String styleName) {
                        this.styleName = styleName;
                }
                public String styleName() { return styleName; }
        };
       
       
        /**
         * This is the entry point method.
         */
        public void onModuleLoad() {
                setupExpenseListLayout();
                setupAddExpenseLayout();
                setupMainLayout();
                updateExpenseListFromServer();
        }

       
        /**
         * 1 of the 2 main display areas of the application...anything to do with adding expenses
         * will be created here.
         */
        private void setupAddExpenseLayout() {
                //create a File Upload for Bulk Expenses
                final VerticalPanel addBulkExpensesLayout = new VerticalPanel();
                addBulkExpensesLayout.setVisible(false);
                addBulkExpensesLayout.addStyleName("Application-componentContent");
                addBulkExpensesLayout.add(new HTML("Each line should follow the format:<br/>MM/dd/yyyy,0.00,Vendor Name,Expense Type,Payment Type"));
                bulkExpenseTextArea.setCharacterWidth(65);
                bulkExpenseTextArea.setVisibleLines(10);
                addBulkExpensesButton.addClickHandler(new ClickHandler() {
                        public void onClick(ClickEvent event) {
                                processBulkExpenses();
                        }
                });
                addBulkExpensesButton.setStyleName("Application-PushButton");
                addBulkExpensesLayout.add(new HTML("<br/>"));
                addBulkExpensesLayout.add(bulkExpenseTextArea);
                addBulkExpensesLayout.add(addBulkExpensesButton);
               
                //create a Grid to add a new Expense...columns are label, input, errorMessage...with an extra row for the submit button
                final FlexTable addIndividualExpenseLayout = new FlexTable();                                          
                addIndividualExpenseLayout.addStyleName("Application-componentContent");
                       
                newExpenseDate.setFormat(dateBoxFormat);
                newExpenseDate.setValue(new Date());
                addIndividualExpenseLayout.setWidget(0, 0, new HTML("Date"));
                addIndividualExpenseLayout.setWidget(0, 1, newExpenseDate);
               
                addIndividualExpenseLayout.setWidget(1, 0, new HTML("Amount"));
                addIndividualExpenseLayout.setWidget(1, 1, newExpenseAmount);

                addIndividualExpenseLayout.setWidget(2, 0, new HTML("Vendor"));
                addIndividualExpenseLayout.setWidget(2, 1, newExpenseVendor);

                addIndividualExpenseLayout.setWidget(3, 0, new HTML("Expense Type"));
                addIndividualExpenseLayout.setWidget(3, 1, newExpenseExpenseType);
                newExpenseExpenseType.addItem("");
                for(int i = 0; i < expenseTypes.length; i++) {
                        newExpenseExpenseType.addItem(expenseTypes[i]);
                }

                addIndividualExpenseLayout.setWidget(4, 0, new HTML("Payment Type"));
                addIndividualExpenseLayout.setWidget(4, 1, newExpensePaymentType);
                newExpensePaymentType.addItem("");
                for(int i = 0; i < paymentTypes.length; i++) {
                        newExpensePaymentType.addItem(paymentTypes[i]);
                }

                //listen for mouse events on the addExpenseButton and do what's needed
                addIndividualExpenseButton.addClickHandler(new ClickHandler() {
                        public void onClick(ClickEvent event) {
                                sendIndividualExpenseToServer();
                        }
                });
                addIndividualExpenseButton.setStyleName("Application-PushButton");
                addIndividualExpenseLayout.setWidget(5, 0, addIndividualExpenseButton);

                //listen for changes to vendorSuggestBox and default to most likely expenseType and paymentType for vendor selected
                newExpenseVendor.addSelectionHandler(new SelectionHandler<SuggestOracle.Suggestion>() {
                        public void onSelection(SelectionEvent<SuggestOracle.Suggestion> event) {
                                Suggestion selectedSuggestion = event.getSelectedItem();
                                String selectedVendorName = selectedSuggestion.getReplacementString();
                                Vendor selectedVendor = vendors.get(selectedVendorName);
                                if(selectedVendor != null) {
                                        String mostLikelyExpenseType = selectedVendor.getLastExpenseType();
                                        String mostLikelyPaymentType = selectedVendor.getLastPaymentType();
                                        setListBoxValueSelection(newExpenseExpenseType, mostLikelyExpenseType);
                                        setListBoxValueSelection(newExpensePaymentType, mostLikelyPaymentType);
                                }
                        }
                });
                       
                //add the list of Vendor for user selection
                budgetService.getVendors(new AsyncCallback<List<Vendor>>() {
                                public void onFailure(Throwable caught) {       // Show the RPC error message to the user
                                        addStatus("Remote Procedure Call Failure: " + caught.getMessage(), StatusType.ERROR);
                                }
       
                                public void onSuccess(List<Vendor> result) {
                                        for(Vendor v : result) {
                                                vendorOracle.add(v.getName());
                                                vendors.put(v.getName(), v);
                                        }
                                }
                });
               
                //layout section that allows switching between individual expense adding and bulk expense adding
                final Hyperlink swapAddExpenseTypeLink = new Hyperlink();
                swapAddExpenseTypeLink.addStyleName("Application-addExpense-header");
                swapAddExpenseTypeLink.setText("Bulk"); //Individual will be displayed to start so make the link for adding Bulk
                swapAddExpenseTypeLink.addClickHandler(new ClickHandler() {
                        public void onClick(ClickEvent event) {
                                if(addIndividualExpenseLayout.isVisible()) {    //current display is for Individual so change it to Bulk
                                        swapAddExpenseTypeLink.setText("Individual");
                                        addBulkExpensesLayout.setVisible(true);
                                        addIndividualExpenseLayout.setVisible(false);
                                }
                                else {  //current display is for Bulk so change it to Individual
                                        swapAddExpenseTypeLink.setText("Bulk");
                                        addBulkExpensesLayout.setVisible(false);
                                        addIndividualExpenseLayout.setVisible(true);
                                }
                        }
                });
               
                //the overall layout for adding any kind of expense
                addExpenseLayout.addStyleName("Application-componentLayout");
                addExpenseLayout.add(swapAddExpenseTypeLink);
                addExpenseLayout.add(addBulkExpensesLayout);
                addExpenseLayout.add(addIndividualExpenseLayout);
        }
       
       
        /**
         * 1 of the 2 main display areas of the application...anything to do with displaying expenses
         * will be created here.
         */
        private void setupExpenseListLayout() {
                //create panels to display expense information and filters for them
                displayExpenseLayout.addStyleName("Application-componentLayout");
                displayExpenseLayout.setHorizontalAlignment(DockPanel.ALIGN_LEFT);
                DisclosurePanel expenseFilterDisclosure = new DisclosurePanel("Expense Filter");        //display filtering options for the expense display
               
                //create the filters for displaying expenses and add their listeners
                FlexTable expenseFilterLayout = new FlexTable();                //components used to filter expenses will horizontally across here
                expenseListStartDate.setFormat(dateBoxFormat);
                expenseListEndDate.setFormat(dateBoxFormat);
                expenseFilterLayout.setText(0, 0, "Expenses from ");
                expenseFilterLayout.setWidget(0, 1, expenseListStartDate);
                expenseFilterLayout.setText(0, 2, " to ");
                expenseFilterLayout.setWidget(0, 3, expenseListEndDate);
                expenseFilterLayout.setText(1, 0, "Vendor");
                expenseFilterLayout.setWidget(1, 1, expenseListVendorFilter);
               
                CheckBox displayList = new CheckBox("show List");
                displayList.setValue(true);
                displayList.addClickHandler(new ClickHandler() {
                        public void onClick(ClickEvent event) {
                                if(((CheckBox)event.getSource()).getValue()) {
                                        expenseListTable.setVisible(true);
                                }
                                else {
                                        expenseListTable.setVisible(false);
                                }
                        }
                });
                CheckBox displaySummary = new CheckBox("show Summary");
                displaySummary.setValue(true);
                displaySummary.addClickHandler(new ClickHandler() {
                        public void onClick(ClickEvent event) {
                                if(((CheckBox)event.getSource()).getValue()) {
                                        expenseSummaryTable.setVisible(true);
                                }
                                else {
                                        expenseSummaryTable.setVisible(false);
                                }
                        }
                });
                expenseFilterLayout.setWidget(1, 2, displayList);
                expenseFilterLayout.setWidget(1, 3, displaySummary);
               
                Date date = new Date();
                CalendarUtil.setToFirstDayOfMonth(date);        //find the 1st day of the current month
                expenseListStartDate.setValue(date);    //default to 1st day of the current month
                CalendarUtil.addMonthsToDate(date, 1);          //find the 1st day of the next month
                CalendarUtil.addDaysToDate(date, -1);           //go back one day to the last day of the current month
                expenseListEndDate.setValue(date);              //default to the last day of the current month
               
                HorizontalPanel expenseFilterButtonBar = new HorizontalPanel();
                PushButton refreshExpensesButton = new PushButton(new Image("refreshIconUp.png"), new Image("refreshIconDown.png"), new ClickHandler() {
                        public void onClick(ClickEvent event) {
                                resetStatus();
                                updateExpenseListFromServer();
                        }
                });
                refreshExpensesButton.setStyleName("Application-PushButton");
                expenseFilterButtonBar.add(refreshExpensesButton);
               
                toggleDeleteExpenseButton.addClickHandler(new ClickHandler() {
                        public void onClick(ClickEvent event) {
                                if(toggleDeleteExpenseButton.isDown()) {
                                        enableDeleteExpenseAbility();
                                }
                                else {
                                        disableDeleteExpenseAbility();
                                }
                        }
                });
                toggleDeleteExpenseButton.setStyleName("Application-PushButton");
                expenseFilterButtonBar.add(toggleDeleteExpenseButton);
                expenseFilterLayout.setWidget(2, 0, expenseFilterButtonBar);
                expenseFilterDisclosure.setAnimationEnabled(true);
                expenseFilterDisclosure.setContent(expenseFilterLayout);
               
                //apply a header and style to the expense list
                expenseListTable.addStyleName("Application-expenseTable");
                expenseListTable.setText(0, 0, "Date");
                expenseListTable.setText(0, 1, "Amount");
                expenseListTable.setText(0, 2, "Vendor");
                expenseListTable.setText(0, 3, "Expense Type");
                expenseListTable.setText(0, 4, "Payment Type");
                expenseListTable.setText(0, 5, "");
                expenseListTable.getRowFormatter().addStyleName(0, "Application-expenseTable-header");
               
                //apply a header and style to the expense summary
                expenseSummaryTable.addStyleName("Application-expenseTable");
                expenseSummaryTable.setText(0, 0, "Expense Summary");
                expenseSummaryTable.getFlexCellFormatter().setColSpan(0, 0, 2);
                expenseSummaryTable.getRowFormatter().addStyleName(0, "Application-expenseTable-header");
               
                //put the components together in the right order...filter across the top with list on the bottom left and summary on the bottom right
                displayExpenseLayout.add(expenseFilterDisclosure, DockPanel.NORTH);
                displayExpenseLayout.add(expenseSummaryTable, DockPanel.WEST);
                displayExpenseLayout.add(expenseListTable, DockPanel.EAST);
        }
       
       
        /**
         * container display to hold the component for adding expenses and
         * the component for displaying expenses
         */
        private void setupMainLayout() {
                //create the Main AbsolutePanel which contains all other layout and associate it with the HTML host page
                VerticalPanel mainLayout = new VerticalPanel();
                mainLayout.setStyleName("Application-mainLayout");
                       
                HTML mainLayoutTitle = new HTML("Family Budgeting");
                mainLayoutTitle.addStyleName("Application-mainLayout-title");
                HTML mainLayoutQuote = new HTML("A penny saved is a penny earned<br/>-Benjamin Franklin");
                mainLayoutQuote.addStyleName("Application-mainLayout-quote");
                HorizontalPanel mainLayoutHeader = new HorizontalPanel();
                mainLayoutHeader.addStyleName("Application-mainLayout-header");
                mainLayoutHeader.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_CENTER);
                mainLayoutHeader.add(mainLayoutTitle);
                mainLayoutHeader.add(mainLayoutQuote);
                       
                statusScroller.addStyleName("Application-componentLayout");
                statusScroller.setHeight("350px");
                statusScroller.setWidth("100%");
                statusScroller.setVisible(false);
                       
                HorizontalPanel mainLayoutSection1 = new HorizontalPanel();
                mainLayoutSection1.add(addExpenseLayout);
                mainLayoutSection1.add(statusScroller);
               
                mainLayout.add(mainLayoutHeader);
                mainLayout.add(mainLayoutSection1);
                mainLayout.add(displayExpenseLayout);
                mainLayout.setCellHeight(displayExpenseLayout, "100%");
                       
                RootPanel.get().add(mainLayout);
                       
                //move the cursor to the item most useful to the user
                newExpenseAmount.setFocus(true);
        }
       
       
        /**
         * Select the item in the ListBox that matches the value...or index 0 if not found
         */
        private void setListBoxValueSelection(ListBox listBox, String value) {
                listBox.setSelectedIndex(0);
                for(int i = 0; i < listBox.getItemCount(); i++) {
                        if(value.equals(listBox.getItemText(i))) {
                                listBox.setSelectedIndex(i);
                                return;
                        }
                }
        }
       
       
        /**
         * Validate that expense information is in the correct format
         */
        private boolean validateExpenseFormat(Date date, String amount, String vendor, String expenseType, String paymentType) {
                boolean validated = true;
               
                if(date == null) {
                        validated = false;
                addStatus("Date must be entered", StatusType.ERROR);
                }
       
        if( ! amount.matches("^\\d*\\.\\d\\d$")) {
                validated = false;
                addStatus("Amount must be entered in format 0.00", StatusType.ERROR);
        }


                if( ! vendor.matches("^\\w[\\w\\.\\-&' ]*$")) {
                validated = false;
                addStatus("Vendor must be entered, begin with a letter or number and contain only letters, numbers, spaces or one of (. & - ')", StatusType.ERROR);
        }
               
                if(Arrays.binarySearch(expenseTypes, expenseType) < 0) {
                validated = false;
                addStatus("Expense Type must match a choice from the Expense Type select box", StatusType.ERROR);
        }
               
                if(Arrays.binarySearch(paymentTypes, paymentType) < 0) {
                validated = false;
                addStatus("Payment Type must match a choice from the Payment Type select box", StatusType.ERROR);
        }
               
                return validated;
        }
       
       
        /**
         * Send the expense to the server and wait for a response.
         */
        private void sendIndividualExpenseToServer() {
                disableChanges();
                resetStatus();
               
                final Date date = newExpenseDate.getValue();
        final String amount = newExpenseAmount.getText();
                final String vendor = newExpenseVendor.getText();
                final String expenseType = newExpenseExpenseType.getItemText(newExpenseExpenseType.getSelectedIndex());
                final String paymentType = newExpensePaymentType.getItemText(newExpensePaymentType.getSelectedIndex());
               
                boolean validated = validateExpenseFormat(date, amount, vendor, expenseType, paymentType);
                if(validated) {
                        //send the info to the server and accept the response from the server
                        final Expense newExpense = new Expense(date, amount, vendor, expenseType, paymentType);
                        budgetService.addExpense(newExpense, new AsyncCallback<String>() {
                                        public void onFailure(Throwable caught) {       // Show the RPC error message to the user
                                                addStatus("Remote Procedure Call Failure: " + caught.getMessage(), StatusType.ERROR);
                                                enableChanges();
                                        }
       
                                        public void onSuccess(String result) {
                                                resetStatus();
                                                vendorOracle.add(vendor);       //add the vendor name as a suggestionBox choice
                                                vendors.put(vendor, new Vendor(vendor, expenseType, paymentType));      //update the vendors list with new information
                                               
                                                //clear out the previous entry
                                                newExpenseDate.setValue(new Date());
                                                newExpenseAmount.setText("");
                                                newExpenseVendor.setText("");
                                                newExpenseExpenseType.setSelectedIndex(0);
                                                newExpensePaymentType.setSelectedIndex(0);
                                               
                                                enableChanges();
                                               
                                                updateExpenseListFromServer();
                                        }
                        });
                }
                else {  //bad data entered...allow the user to fix it.
                        enableChanges();
                }
        }

       
        /**
         * Get the input from the bulkExpenseText Area.
         * Send the expenses to the server in batches of 25 and wait for a response.
         * The expensesString must be in the format:
         *      MM/dd/yyyy,0.00,Vendor Name,Expense Type,Payment Type\n
         * All valid expenses will be submitted up to an incorrect format which will stop
         * processing at that point and an error will be displayed.
         */
        private void processBulkExpenses() {
                resetStatus();
               
                //no changes during processing...it will be re-enabled in the sendBulkExpensesToServer method on failure or completion of bulk expenses
                bulkExpenseTextArea.setCursorPos(0);    //display top expense as they are being added
                disableChanges();
               
                //try to send expenses to the server
                sendBulkExpensesToServer();
        }
       
       
        /**
         * Grab 25 (or as many as possible) bulk expenses from the text box and
         * send them to the server
         */
        private void sendBulkExpensesToServer() {
                final List<Expense> bulkExpenses = new ArrayList<Expense>();    //will be thrown away if any lines fail validation
                boolean validated = true;
               
                //take chunks of 25 for processing (in the split the 26th entry will contain the remaining unprocessed bulk expenses if they exist)
                String bulkExpenseString = bulkExpenseTextArea.getText();
                String[] expenseStrings = bulkExpenseString.split("\r\n|[\r\n]", 26);   //split on \r\n if they are together or either \r or \n if only one of them appears
                int batchSize = expenseStrings.length > 25 ? 25 : expenseStrings.length;
                final String remainingBulkExpenseString = expenseStrings.length > 25 ? expenseStrings[25] : "";
               
                if(batchSize > 0) {     //if batchSize is 0 then all bulk expenses have been processed and the recursion can fall out
                        try {
                                addStatus("Validating " + batchSize + " bulk expenses", StatusType.STATUS);
                                for(int i = 0; i < batchSize; i++) {
                                        int lineNumber = i + 1;
                                        String expenseString = expenseStrings[i];
                                        String[] expenseComponents = expenseString.split(",");
                                        if(expenseComponents.length == 5) {
                                                Date date = null;
                                                try {
                                                        date = dateFormat.parse(expenseComponents[0]);
                                                }
                                                catch(IllegalArgumentException e) {
                                                        validated = false;
                                                        addStatus("Line " + lineNumber + ": date is not formatted MM/dd/yyyy", StatusType.ERROR);
                                                }
                                                String amountString = expenseComponents[1];
                                                String vendorString = expenseComponents[2];
                                                String expenseTypeString = expenseComponents[3];
                                                String paymentTypeString = expenseComponents[4];
                                                if(validateExpenseFormat(date, amountString, vendorString, expenseTypeString, paymentTypeString)) {
                                                        bulkExpenses.add(new Expense(date, amountString, vendorString, expenseTypeString, paymentTypeString));
                                                }
                                                else {
                                                        validated = false;
                                                        addStatus("Line " + lineNumber + ": contains errors", StatusType.ERROR);        //the actual errors are added to the status in the validateExpenseFormat call
                                                }
                                        }
                                        else {
                                                validated = false;
                                                addStatus("Line " + lineNumber + ": is not formatted correctly with 5 comma separated values", StatusType.ERROR);
                                        }
                                }
                        }
                        catch(Exception e) {
                                validated = false;
                        addStatus(e.getMessage(), StatusType.ERROR);
                        }
       
                        //if all the lines passed validation then send them to the server
                        if(validated) {
                                //send the info to the server and accept the response from the server
                                addStatus("Sending " + batchSize + " bulk expenses to the server", StatusType.STATUS);
                                budgetService.addExpenses(bulkExpenses, new AsyncCallback<String>() {
                                                public void onFailure(Throwable caught) {       // Show the RPC error message to the user
                                                        addStatus("Remote Procedure Call Failure: " + caught.getMessage(), StatusType.ERROR);
                                                        enableChanges();        //allow the user to fix the problem
                                                }
                       
                                                public void onSuccess(String result) {
                                                        bulkExpenseTextArea.setText(remainingBulkExpenseString);
                                                        addStatus("Successfully added Bulk Expenses", StatusType.STATUS);
                                                        for(Expense e : bulkExpenses) {
                                                                vendorOracle.add(e.getVendor());        //add the vendor name as a suggestionBox choice
                                                                vendors.put(e.getVendor(), new Vendor(e.getVendor(), e.getExpenseType(), e.getPaymentType()));  //update the vendors list with new information
                                                        }

                                                        if(remainingBulkExpenseString.length() > 0) {   //there are more bulk expenses to process
                                                                sendBulkExpensesToServer();
                                                        }
                                                        else {  //all done with bulk expenses
                                                                enableChanges();
                                                        }
                                                }
                                });
                        }
                        else { //the expenses did not validate so allow the user to fix the problem
                                enableChanges();
                        }
                }
        }

       
        /**
         * during some processing the user shouldn't be making other changes...
         * disable buttons temporarily
         */
        private void disableChanges() {
                bulkExpenseTextArea.setEnabled(false);
                addBulkExpensesButton.setEnabled(false);        
                addIndividualExpenseButton.setEnabled(false);
        }
       
       
        /**
         * when it is safe for the user to make more changes...
         */
        private void enableChanges() {
                bulkExpenseTextArea.setEnabled(true);
                addBulkExpensesButton.setEnabled(true);
                addIndividualExpenseButton.setEnabled(true);
        }
       
       
        /**
         * Request a List of Expenses from the server and display them
         */
        private void updateExpenseListFromServer() {
                //validate that the needed information is present
                boolean validated = true;
               
                final String vendorFilter = expenseListVendorFilter.getText();
                final Date startDate = expenseListStartDate.getValue();
                final Date endDate = expenseListEndDate.getValue();
               
                if(startDate == null || endDate == null) {
                        validated = false;
                addStatus("Start and End Date must be entered to display Expenses", StatusType.ERROR);
                }
       
                if(validated) {
                        //send the info to the server and accept the response from the server
                        budgetService.getExpenses(startDate, endDate, vendorFilter, new AsyncCallback<List<Expense>>() {
                                        public void onFailure(Throwable caught) {       // Show the RPC error message to the user
                                                addStatus("Remote Procedure Call Failure: " + caught.getMessage(), StatusType.ERROR);
                                        }
       
                                        public void onSuccess(List<Expense> result) {
                                                displayExpenseList(result);
                                                displayExpenseSummary(result);
                                        }
                        });
                }
        }



        /**
         * Delete an Expense from the server and the display list
         */
        private void deleteExpenseFromServer(long expenseId) {
                resetStatus();
               
                //send the info to the server and accept the response from the server
                budgetService.deleteExpense(expenseId, new AsyncCallback<String>() {
                                public void onFailure(Throwable caught) {       // Show the RPC error message to the user
                                        addStatus("Remote Procedure Call Failure: " + caught.getMessage(), StatusType.ERROR);
                                }
       
                                public void onSuccess(String result) {
                                        resetStatus();
                                        disableDeleteExpenseAbility();
                                        updateExpenseListFromServer();
                                }
                });
        }

       
        /**
         * enable Delete Expense buttons
         */
        private void enableDeleteExpenseAbility() {
                toggleDeleteExpenseButton.setDown(true);
                if(deleteExpenseButtons != null) {
                        for(PushButton button : deleteExpenseButtons) {
                                button.setVisible(true);
                        }
                }
        }
       
       
        /**
         * disable Delete Expense buttons
         */
        private void disableDeleteExpenseAbility() {
                toggleDeleteExpenseButton.setDown(false);
                if(deleteExpenseButtons != null) {
                        for(PushButton button : deleteExpenseButtons) {
                                button.setVisible(false);
                        }
                }
        }
       
       
        /**
         * add a New Expense to the list of Expenses in addExpenseLayout
         */
        private void displayExpenseList(List<Expense> expenses) {
                resetExpenseTable(expenseListTable);    //clear the previous List of expenses
                deleteExpenseButtons = new ArrayList<PushButton>();     //new expense List so a new List of delete buttons
               
                int row = 1;
                for(Expense e : expenses) {
                        //before adding to the List make a button to delete the expense...not visible unless user requests delete option
                        final Long expenseId = e.getId();
                        PushButton deleteExpenseButton = new PushButton(new Image("deleteIconSmallUp.png"), new Image("deleteIconSmallDown.png"), new ClickHandler() {
                                public void onClick(ClickEvent event) {
                                        deleteExpenseFromServer(expenseId);
                                }
                        });
                        deleteExpenseButton.setStyleName("Application-PushButton");
                        deleteExpenseButton.setVisible(toggleDeleteExpenseButton.isDown());
                        deleteExpenseButtons.add(deleteExpenseButton);
                       
                        //now display the expense in the List
                        expenseListTable.setText(row, 0, dateFormat.format(e.getDate()));
                        expenseListTable.setText(row, 1, e.getDisplayAmount());
                        expenseListTable.getCellFormatter().addStyleName(row, 1, "Application-expenseTable-amount");
                        expenseListTable.setText(row, 2, e.getVendor());
                        expenseListTable.setText(row, 3, e.getExpenseType());
                        expenseListTable.setText(row, 4, e.getPaymentType());
                        expenseListTable.setWidget(row, 5, deleteExpenseButton);
                        row++;
                }
        }
       
       
        /**
         * add a New Expense to the summary of Expenses in addExpenseLayout
         * Map<String, String> 1st String is the ExpenseType, 2nd String is the Amount
         */
        private void displayExpenseSummary(List<Expense> expenses) {
                resetExpenseTable(expenseSummaryTable);
               
                Map<String, Integer> expenseTypeMap = new HashMap<String, Integer>();   //new Expense summary so new Map to hold the info
               
                int totalExpenses = 0;
               
                for(Expense e : expenses) {
                        totalExpenses = totalExpenses + e.getAmount();
                       
                        Integer expenseTypeAmount = expenseTypeMap.get(e.getExpenseType());
                        if(expenseTypeAmount == null) { //this expenseType is new to the Map
                                expenseTypeMap.put(e.getExpenseType(), e.getAmount());
                        }
                        else {  //accumulate on the existing expenseType in the Map
                                expenseTypeMap.put(e.getExpenseType(), e.getAmount() + expenseTypeAmount);
                        }
                }
               
                expenseSummaryTable.setText(1, 0, "Total");
                expenseSummaryTable.setText(1, 1, Expense.convertAmountToDisplay(totalExpenses));
                expenseSummaryTable.getRowFormatter().addStyleName(1, "Application-expenseTable-total");
                expenseSummaryTable.getCellFormatter().addStyleName(1, 1, "Application-expenseTable-amount");
               
                //insert items in FlexTable ordered from most expensive to least expensive
                int row = 2;    //row 0 is header, row 1 is total...items start from row 2
                for(String expenseType : expenseTypeMap.keySet()) {
                        int insertRowIndex = row;       //default to a new row after all existing rows  
                       
                        Integer expenseTypeTotal = expenseTypeMap.get(expenseType);
                       
                        for(int rowCounter = row; rowCounter > 2; rowCounter--) {       //loop through all of the rows until a larger value is found than the new one and insert the row right after that
                                Integer compareToAmount = Expense.convertDisplayToAmount(expenseSummaryTable.getText(rowCounter - 1, 1));
                                if(expenseTypeTotal.compareTo(compareToAmount) > 0) {   //our number is bigger than the one in this row we will mark it and look at the next row
                                        insertRowIndex = rowCounter - 1;
                                }
                                else {  //our number isn't this big so we can insert it where we are...no need to keep looking
                                        break;
                                }
                        }
                       
                        expenseSummaryTable.insertRow(insertRowIndex);
                        expenseSummaryTable.setText(insertRowIndex, 0, expenseType);
                        expenseSummaryTable.setText(insertRowIndex, 1, Expense.convertAmountToDisplay(expenseTypeTotal));
                        expenseSummaryTable.getCellFormatter().addStyleName(insertRowIndex, 1, "Application-expenseTable-amount");
                       
                        row++;
                }
        }
       
       
        /**
         * Used to remove data from either expenseListTable or expenseSummaryTable leaving
         * the 1st row which is the header information.  FlexTable.clear only gets rid of
         * Widgets not Text...so this resets a flex table for new data
         * @param ft
         */
        private void resetExpenseTable(FlexTable ft) {
                while(ft.getRowCount() > 1) {
                        ft.removeRow(1);        //always remove row 1 because the size of the FlexTable is 1 less each time and row 0 is header information
                }
        }
       
        /**
         * Display messages to the user
         */
        private void addStatus(String message, StatusType statusType) {
                HTML status = new HTML(message);
                status.addStyleName(statusType.styleName());
                statusLayout.add(status);
                statusScroller.scrollToBottom();
                statusScroller.setVisible(true);
        }
       
        /**
         * Remove old status messages and don't display status area
         */
        private void resetStatus() {
                statusLayout.clear();
                statusScroller.setVisible(false);
        }
}